/*
 * cryptomfs - a macfuse implementation of cryptofs.
 * Copyright (C) 2008  Greg J. Hedlund
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define MAXPASSWORDLENGTH 256

#include "cryptomfspw.h"

#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include <termios.h>

#include <glib.h>
#include <gcrypt.h>

#define MAX_AUTH_ATTEMPTS 3

static const char* keychainAcctName = "cryptomfs";
static char* keychainWhere;

static CryptomfsConfig *config;
static char *configFile;

static int authMode;

static char* cmdPassword;

static int authAttempts = 0;


void cryptomfs_set_authenticaion_mode(int mode) 
{
	authMode = mode;
}

int cryptomfs_get_authentication_mode()
{
	return authMode;
}

// utility for copying n characters from source to dest
// freeing dest if necessary.  This method ensures that
// dest is null terminated.  The return value is the 
// number of bytes copied.
static size_t free_and_copy(char** dest, const char* source, size_t n)
{
	if(*dest != NULL)
		free(*dest);
		
	if(source == NULL) {
		*dest = NULL;
		return 0;
	} else {
		*dest = (char*)calloc(n+1, sizeof(char)); // extra byte for NUL char
		return strlcpy(*dest, source, n+1);
	}
}

void cryptomfs_set_config(char *file, CryptomfsConfig *cfg)
{
	free_and_copy(&configFile, file, strlen(file));
	config = cfg;
}

void cryptomfs_set_keychain_where(const char* value)
{
	free_and_copy(&keychainWhere, value, strlen(value));
}

static void pfree(char* password)
{
	if(password) {
		memset(password, 0xF, MAXPASSWORDLENGTH);
		munlock(password, MAXPASSWORDLENGTH);
		free(password);
	}
}

static void showError(const char* msg)
{
	// print to stderr
	fprintf(stderr, "mount_cryptomfs: %s\n", msg);
	
	// show a dialog if in UI mode
	if(authMode == _AUTH_MODE_UI_)
	{
		// display an alert dialog
		CFStringRef alertHeader = CFSTR("Mount Failed");
		CFStringRef alertMessage = 
			CFStringCreateWithCString(kCFAllocatorDefault, msg, kCFStringEncodingUTF8);
		
		CFUserNotificationDisplayAlert(0,
									   kCFUserNotificationCautionAlertLevel,
									   NULL, NULL, NULL,
									   alertHeader,
									   alertMessage,
									   NULL, NULL, NULL,
									   kCFUserNotificationDefaultResponse);
	}
}

static OSStatus cryptomfs_store_keychain_password(void *password, UInt32 passwordLength)
{
	if(keychainWhere == NULL)
		return 1;
		
	OSStatus retVal = noErr;
	
	retVal = 
		SecKeychainAddGenericPassword(
			NULL, // default keychain
			strlen(keychainWhere), keychainWhere,
			strlen(keychainAcctName), keychainAcctName,
			passwordLength, password,
			NULL);
	
	return retVal;
}

static OSStatus cryptomfs_get_keychain_password(void *password, UInt32 *passwordLength)
{
	if(keychainWhere == NULL)
		return 1;
		
	OSStatus retVal = noErr;
	
	retVal = 
		SecKeychainFindGenericPassword(
			NULL, // default keychain
			strlen(keychainWhere), keychainWhere, // service name
			strlen(keychainAcctName), keychainAcctName, // account name
			passwordLength, password, // password
			NULL); // item ref
	return retVal;
}

static char *getpasswordhash(const char *password)
{
	int digestLen;
	unsigned char *digest;
	char *retVal;
	
	// create password digest
	digestLen = gcry_md_get_algo_dlen(GCRY_MD_MD5);
	digest = (unsigned char*)calloc(digestLen, sizeof(char));
	gcry_md_hash_buffer(GCRY_MD_MD5, digest, password, strlen(password));
	
	// convert digest to base64
	retVal = (char*)g_base64_encode(digest, digestLen);
	
	free(digest);
	
	return retVal;
}

static passwordReturn_t getpwdusingterminal(const char *msg, char *password)
{
	struct termios old, new;
	int i;
	char *p;
	
	printf("%s", msg); fflush(stdout);
	
	// get old term settings and turn
	// off ECHO
	tcgetattr(0, &old);
	new = old;
	new.c_lflag &= ~(ECHO);
	tcsetattr(0, TCSAFLUSH, &new);
	
	password[0] = 0;
	i = read(0, password, MAXPASSWORDLENGTH-1);
	if(i >= 0)
		password[i] = 0;
	if((p = strchr(password, '\n'))) 
		*p = 0;
	
	// reset term settings
	tcsetattr(0, TCSAFLUSH, &old);
	putchar('\n');
	fflush(stdout);
	
	return kCMFSResponseOk;
}

static passwordReturn_t newpwdusingterminal(char *password) 
{
	char *newPass;
	char *passwordHash;
	passwordReturn_t retVal = kCMFSResponseOk;
	
	getpwdusingterminal("Enter new password: ", password);
	
	newPass = (char*)calloc(MAXPASSWORDLENGTH, sizeof(char));
	mlock(newPass, MAXPASSWORDLENGTH);
	
	getpwdusingterminal("Re-enter password: ", newPass);
	
	if(strcmp(newPass, password) != 0)
	{
		pfree(password);
		password = NULL;
		retVal = kCMFSResponsePasswordMatchError;
	} else {
		passwordHash = getpasswordhash(password);
		
		strncpy(config->passwordhash, passwordHash, strlen(passwordHash));
		
		free(passwordHash);
		
		writeConfig(configFile, config);
	}
	pfree(newPass);
	return retVal;
}

static passwordReturn_t getpwdusingdialog(char **password) {
	CFUserNotificationRef passwordDialog;
	SInt32 error;
	CFDictionaryRef dialogTemplate;
	CFOptionFlags responseFlags;
	int button;
	CFStringRef passwordRef;
	//CFIndex passwordMaxSize;
	//char *password;
	SInt32 savePassword;
	
	if(keychainWhere != NULL) {
		// try to find the keychain password first
		void* testPass = NULL;
		UInt32 testPassLength;
		
		if(cryptomfs_get_keychain_password(&testPass, &testPassLength) == noErr) {
			// copy password data
			// password = (char*)calloc(testPassLength, sizeof(char));
			strncpy(*password, (char*)testPass, testPassLength);
			
			SecKeychainItemFreeContent(
				NULL,
				testPass);
				
			//return password;
			return kCMFSResponseOk;
		}
	}
	
	char* dialogMessage = "Enter password: ";
	char* dialogIcon = NULL;
	unsigned int freeIconStr = 0;
	if (g_str_has_prefix(config->volumeIcon, CRYPT_PKG)) {
		gchar * suffix = (config->volumeIcon + strlen(CRYPT_PKG)+1);
		dialogIcon = g_strjoin("/", config->packageroot, suffix, NULL);
		freeIconStr = 1;
	} else {
		dialogIcon = config->volumeIcon;
	}
	
	char* dialogHeader = "Password Entry";
		
	CFURLRef dialogIconURL = NULL;
	if(strlen(dialogIcon) > 0) 
	{
		dialogIconURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
			CFStringCreateWithCString(kCFAllocatorDefault, dialogIcon, kCFStringEncodingUTF8),
			kCFURLPOSIXPathStyle, false);
	}
	
	CFStringRef headerString  =
		CFStringCreateWithCString(kCFAllocatorDefault, dialogHeader, kCFStringEncodingUTF8);
		
	CFStringRef msgString = 
		CFStringCreateWithCString(kCFAllocatorDefault, dialogMessage, kCFStringEncodingUTF8);

	int arraySize = (dialogIconURL == NULL ? 4 : 5);

	int keyIdx = 0;
	void** keys = (void**)calloc(arraySize, sizeof(void*));
	if(dialogIconURL) {
		keys[keyIdx++] = (void*) kCFUserNotificationIconURLKey;
	}
	keys[keyIdx++] = (void*)kCFUserNotificationAlertHeaderKey;
	keys[keyIdx++] = (void*)kCFUserNotificationAlertMessageKey;
	keys[keyIdx++] = (void*)kCFUserNotificationTextFieldTitlesKey;
	keys[keyIdx++] = (void*)kCFUserNotificationCheckBoxTitlesKey;
	
	int valIdx = 0;
	void** values =  (void**)calloc(arraySize, sizeof(void*));
	if(dialogIconURL) {
		values[valIdx++] = (void*)dialogIconURL;
	}
	values[valIdx++] = (void*)headerString;
	values[valIdx++] = (void*)msgString;
	values[valIdx++] = (void*)CFSTR("Password");
	values[valIdx++] = (void*)CFSTR("Save password in Keychain");

	dialogTemplate = CFDictionaryCreate(kCFAllocatorDefault,
									  (const void**)keys,
									  (const void**)values,
									  arraySize,
									  &kCFTypeDictionaryKeyCallBacks,
									  &kCFTypeDictionaryValueCallBacks);
									  
	passwordDialog = CFUserNotificationCreate(kCFAllocatorDefault,
											0,
											kCFUserNotificationPlainAlertLevel
											  |
											CFUserNotificationSecureTextField(0),
											&error,
											dialogTemplate);
											
	if(error) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseAllocationError;
	}
	
	error = CFUserNotificationReceiveResponse(passwordDialog,
											0,
											&responseFlags);
	if (error) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseOtherError;
	}

	button = responseFlags & 0x3;
	if (button == kCFUserNotificationAlternateResponse) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseCanceled;
	}

	passwordRef = CFUserNotificationGetResponseValue(passwordDialog,
												   kCFUserNotificationTextFieldValuesKey,
												   0);

	//passwordMaxSize = CFStringGetMaximumSizeForEncoding(CFStringGetLength(passwordRef),
	//												  kCFStringEncodingUTF8);
	//password = (char*)calloc(passwordMaxSize, sizeof(char));
	CFStringGetCString(passwordRef,
					 *password,
					 MAXPASSWORDLENGTH,
					 kCFStringEncodingUTF8);
					 
	// see if we should save the password in the keychain
	savePassword = responseFlags & CFUserNotificationCheckBoxChecked(0);
	
	if(savePassword && keychainAcctName != NULL) {
		cryptomfs_store_keychain_password(*password, strlen(*password));
	}
	
	free(keys);
	free(values);
	if(freeIconStr)
		free(dialogIcon);
	CFRelease(passwordDialog);
	if(dialogIconURL)
		CFRelease(dialogIconURL);
	CFRelease(headerString);
	CFRelease(msgString);
	
	return kCMFSResponseOk;
}

static passwordReturn_t newpwdusingdialog(char **password) {
	CFUserNotificationRef passwordDialog;
	SInt32 error;
	CFDictionaryRef dialogTemplate;
	CFOptionFlags responseFlags;
	int button;
	CFStringRef passwordRef;
	CFStringRef repassRef;
	SInt32 savePassword;
	char *passwordHash;
	
	char* dialogMessage = "Enter password: ";
	char* dialogIcon = NULL;
	unsigned int freeIconStr = 0;
	if (g_str_has_prefix(config->volumeIcon, CRYPT_PKG)) {
		gchar * suffix = (config->volumeIcon + strlen(CRYPT_PKG)+1);
		dialogIcon = g_strjoin("/", config->packageroot, suffix, NULL);
		freeIconStr = 1;
	} else {
		dialogIcon = config->volumeIcon;
	}
	
	char* dialogHeader = "Password Entry";
	
	CFURLRef dialogIconURL = NULL;
	if(strlen(dialogIcon) > 0)
	{
		dialogIconURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
								  CFStringCreateWithCString(kCFAllocatorDefault, dialogIcon, kCFStringEncodingUTF8),
								  kCFURLPOSIXPathStyle, false);
	}
	
	CFStringRef headerString  =
	CFStringCreateWithCString(kCFAllocatorDefault, dialogHeader, kCFStringEncodingUTF8);
	
	CFStringRef msgString = 
	CFStringCreateWithCString(kCFAllocatorDefault, dialogMessage, kCFStringEncodingUTF8);
	
	CFStringRef textFieldVals[2];
	textFieldVals[0] = CFSTR("Password"); 
	textFieldVals[1] = CFSTR("Re-enter password");
	CFArrayRef cfTextFieldVals = CFArrayCreate(kCFAllocatorDefault,
									 (const void**)textFieldVals,
									 2,
									 &kCFTypeArrayCallBacks);
	
	size_t arraySize = (dialogIconURL == NULL ? 4 : 5);
	
	int keyIdx = 0;
	void **keys = (void**)calloc(arraySize, sizeof(void*));
	if(dialogIconURL) {
		keys[keyIdx++] = (void*)kCFUserNotificationIconURLKey;
	}
	keys[keyIdx++] = (void*)kCFUserNotificationAlertHeaderKey;
	keys[keyIdx++] = (void*)kCFUserNotificationAlertMessageKey;
	keys[keyIdx++] = (void*)kCFUserNotificationTextFieldTitlesKey;
	keys[keyIdx++] = (void*)kCFUserNotificationCheckBoxTitlesKey;
	
	//const void* keys[] = {
//		kCFUserNotificationIconURLKey,
//		kCFUserNotificationAlertHeaderKey,
//		kCFUserNotificationAlertMessageKey,
//		kCFUserNotificationTextFieldTitlesKey,
//		kCFUserNotificationCheckBoxTitlesKey
//	};
	
	int valIdx = 0;
	void **values = (void**)calloc(arraySize, sizeof(void*));
	if(dialogIconURL) {
		values[valIdx++] = (void*)dialogIconURL;
	}
	values[valIdx++] = (void*)headerString;
	values[valIdx++] = (void*)msgString;
	values[valIdx++] = (void*)cfTextFieldVals;
	values[valIdx++] = (void*)CFSTR("Save password in Keychain");
	
	//const void* values[] = {
//		dialogIconURL,
//		headerString,
//		msgString,
//		cfTextFieldVals,
//		CFSTR("Save password in Keychain")
//	};
	
	dialogTemplate = CFDictionaryCreate(kCFAllocatorDefault,
										(const void**)keys,
										(const void**)values,
										arraySize,
										&kCFTypeDictionaryKeyCallBacks,
										&kCFTypeDictionaryValueCallBacks);
	
	passwordDialog = CFUserNotificationCreate(kCFAllocatorDefault,
											  0,
											  CFUserNotificationSecureTextField(0)
											  |
											  CFUserNotificationSecureTextField(1),
											  &error,
											  dialogTemplate);
	
	if(error) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseAllocationError;
	}
	
	error = CFUserNotificationReceiveResponse(passwordDialog,
											  0,
											  &responseFlags);
	if (error) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseOtherError;
	}
	
	button = responseFlags & 0x3;
	if (button == kCFUserNotificationAlternateResponse) {
		pfree(*password);
		*password = NULL;
		return kCMFSResponseCanceled;
	}
	
	passwordRef = CFUserNotificationGetResponseValue(passwordDialog,
													 kCFUserNotificationTextFieldValuesKey,
													 0);
	
	repassRef = CFUserNotificationGetResponseValue(passwordDialog, 
												   kCFUserNotificationTextFieldValuesKey, 1);
	
	// make sure passwords match
	if(CFStringCompare(passwordRef, repassRef, 0) != 0) 
	{
		pfree(*password);
		*password = NULL;
		return kCMFSResponsePasswordMatchError;
	}
	
	CFStringGetCString(passwordRef,
					   *password,
					   MAXPASSWORDLENGTH,
					   kCFStringEncodingUTF8);
	
	// write password hash to config
	passwordHash = getpasswordhash(*password);
	strncpy(config->passwordhash, passwordHash, strlen(passwordHash));
	
	writeConfig(configFile, config);
	
	// see if we should save the password in the keychain
	savePassword = responseFlags & CFUserNotificationCheckBoxChecked(0);
	
	if(savePassword && keychainAcctName != NULL) {
		cryptomfs_store_keychain_password(*password, strlen(*password));
	}
	
	free(keys);
	free(values);
	free(passwordHash);
	if(freeIconStr)
		free(dialogIcon);
	CFRelease(passwordDialog);
	if(dialogIconURL)
		CFRelease(dialogIconURL);
	CFRelease(headerString);
	CFRelease(msgString);
	
	return kCMFSResponseOk;
}

static char *_getPassword()
{
	char *passwordHash;
	passwordReturn_t err = kCMFSResponseOtherError;
	
	if(authAttempts++ == MAX_AUTH_ATTEMPTS)
	{
		showError("Max password attempts reached, aborting mount.");
		exit(1);
	}
	
	char *password = (char*)malloc(MAXPASSWORDLENGTH * sizeof(char));
	mlock(password, MAXPASSWORDLENGTH);
	if(authMode == _AUTH_MODE_UI_)
	{
		if(strlen(config->passwordhash) == 0)
			err = newpwdusingdialog(&password);
		else
			err = getpwdusingdialog(&password);
	} else if(authMode == _AUTH_MODE_CONSOLE_) {
		if(strlen(config->passwordhash) == 0)
			err = newpwdusingterminal(password);
		else
			err = getpwdusingterminal("Enter password: ", password);
	}
	
	if(err == kCMFSResponseOk)
	{
		passwordHash = getpasswordhash(password);
		if(strcmp(passwordHash, config->passwordhash) != 0)
		{
			showError("Incorrect password, please try again.");
			free(passwordHash);
			pfree(password);
			return _getPassword();
		}
		free(passwordHash);
	} else {
		switch(err) {
			case kCMFSResponseAllocationError:
				showError("Out of memory.");
				break;
				
			case kCMFSResponsePasswordMatchError:
				showError("Passwords do not match, please try again.");
				return _getPassword();
				break;
				
			case kCMFSResponseCanceled:
				showError("User canceled mount.");
				break;
				
			default:
				showError("Unknown error occured, aborting mount.");
		}
		
		// if we get here we want to abort the mount
		exit(1);
	}
	
	return password;
}

static char *getPassword(void)
{
	authAttempts = 0;
    return _getPassword();
}

static void freePassword(char *password)
{
    if(password != NULL)
		pfree(password);
	
	if(keychainWhere != NULL)
		free(keychainWhere);
}

static char* getCheckedPassword()
{
	return cmdPassword;
}

static void doNothing()
{}

static PasswordQuery copyQuery =
{
	getCheckedPassword,
	doNothing
};

PasswordQuery *cryptomfs_get_copy_password_query(char *pwd)
{
	free_and_copy(&cmdPassword, pwd, strlen(pwd));
	return &copyQuery;
}

static PasswordQuery query =
{
    getPassword,
    freePassword
};

PasswordQuery *cryptomfs_get_default_password_query() {
	return &query;
}
